package org.openzip.openzip.gui.util;


import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.*;

import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.Icon;
import javax.swing.JCheckBox;
import javax.swing.JFrame;

import org.openzip.openzip.gui.util.event.ButtonStateChangedListener;

/**
 * An extension of <code>ThreestateCheckBox</code> to support "slaves". The
 * state of the "master" checkbox is determined by the state of its "slave"
 * checkboxes:<br>
 * 1) If all slave checkboxes are selected, then master is also selected.<br>
 * 2) If all slave checkboxes are unselected, then master is also unselected<br>
 * 3) If at least one, but not all, slave is selected, the master is in the
 * third state - undecided.<br>
 * 4) If at least one of the checkboxes is a <code>ThreestateCheckBox</code> and
 * this checkbox is "undecided", then the master's state is also "undecided", no
 * matter what state other slaves are in. <br>
 * <br>
 * When a master is clicked, it will set all its slaves to the new state.
 * Setting master's state to "undecided" will not cause all slaves to go
 * "undecided" as well.<br>
 * The slaves' states are determined by adding an <code>ActionListener</code> (
 * <code>ButtonStateChangedListener</code> in case of
 * <code>ThreestateCheckBox</code>) to each slave.<br>
 * Many members that would normally be "private" are "protected" in this class.
 * This is to support future extends as this class is open-source.
 * 
 * @author Jakub Závěrka
 */
public class ObserverThreestateCheckBox extends ThreestateCheckBox
{	
	protected Map<JCheckBox, CheckBoxListener> slavesMap;
	protected ActionListenerPool listenersPool = new ActionListenerPool();
	
	/**
	 * Constructor of the ObserverCheckBox.
	 * @param text Text of the checkbox.
	 * @param icon Icon of the checkbox.
	 * @param slaves Collection of the "slave" buttons.
	 */
	public ObserverThreestateCheckBox(String text, Icon icon, 
				Collection<JCheckBox> slaves)
	{
		super(text);
		slavesMap = new HashMap<JCheckBox, CheckBoxListener>();
		for(JCheckBox box : slaves){
			addSlave(box);
		}
		stateChanged();
	}

	public ObserverThreestateCheckBox(String text, Collection<JCheckBox> slaves){
		this(text, null, slaves);
	}

	public ObserverThreestateCheckBox(Collection<JCheckBox> children){
		this(null, children);
	}
	
	public ObserverThreestateCheckBox(String text){
		this(text, new HashSet<JCheckBox>());
	}
	
	public ObserverThreestateCheckBox(){
		this(new HashSet<JCheckBox>());
	}

	/**
	 * Adds a slave checkbox to this ObserverCheckBox. The state will be 
	 * re-calculated.
	 * @param checkBox Slave <code>JCheckBox</code>
	 */
	public void addSlave(JCheckBox checkBox){
		CheckBoxListener listener = listenersPool.getInstance();
		listener.setCheckBox(checkBox);
		slavesMap.put(checkBox, listener);
		stateChanged();
	}

	/**
	 * Will return a set of <code>JCheckBox</code>es that are set as slaves to
	 * this master. The returned set is a new instance.
	 * @return
	 */
	public Collection<JCheckBox> getSlaves(){
		return new HashSet<JCheckBox>(slavesMap.keySet());
	}
	
	public void setSlaves(Collection<JCheckBox> slaves) {
		for (JCheckBox slave : slaves) {
			CheckBoxListener listener = listenersPool.getInstance();
			listener.setCheckBox(slave);
			slavesMap.put(slave, listener);
		}
		stateChanged();
	}

	/**
	 * Removes a slave checkbox from this ObserverCheckBox. The state will be 
	 * re-calculated.
	 * @param checkBox Slave <code>JCheckBox</code>
	 */
	public void removeChild(JCheckBox checkBox){
		CheckBoxListener listener = slavesMap.get(checkBox);
		slavesMap.remove(listener);
		listenersPool.returnInstance(listener);
		stateChanged();
	}
	
	/**
	 * This method sets the "super" state of this checkbox. This method is used
	 * when the state is calculated and needs to be set. Ordinary
	 * <code>setState</code> will perform the recalculation based on slave
	 * checkboxes and cannot be used.
	 * 
	 * @param state
	 *           New "super" state of this checkbox.
	 */
	protected void setSuperState(State state){
		super.setState(state);
	}
	
	/**
	 * Will set the state of this <code>ObserverThreestateCheckBox</code> to
	 * the new state. Then, this state is copied to all slaves (except for the
	 * "undecided" state).
	 */
	@Override public void setState(State newState){
		if(newState == State.UNDECIDED){
			return;
		}
		
		super.setState(newState);
		if(slavesMap != null){
			for(CheckBoxListener listener : slavesMap.values()){
				listener.setActivated(false);
			}
			for(JCheckBox checkBox : slavesMap.keySet()){
				checkBox.setSelected(newState.isSelected());
			}
			for(CheckBoxListener listener : slavesMap.values()){
				listener.setActivated(true);
			}
		}
	}
	
	/**
	 * This method is called when a change in slaves' states occured and a
	 * button would possibly needed a repaint.<br>
	 * This method calculates the state from the slaves and sets the apropriate
	 * state by calling <code>setSuperState()</code>.
	 */
	protected void stateChanged(){
		if(slavesMap.size() == 0){
			return;
		}
		CheckBoxListener[] listeners = slavesMap.values().toArray(
					new CheckBoxListener[slavesMap.size()]);
		State firstState = listeners[0].getCurrentState();
		
		if(firstState == State.UNDECIDED){
			setSuperState(State.UNDECIDED);
			return;
		}
		
		for(int i = 1; i < listeners.length; i++){
			if(firstState != listeners[i].getCurrentState())
			{
				setSuperState(State.UNDECIDED);
				return;
			}
		}
		setSuperState(firstState);
	}
	
	/**
	 * This is a "pool" class holding <code>CheckBoxListener</code>s for the
	 * <code>ObserverThreestateCheckBox</code>. This "pool" implementation is
	 * "endless" - there is no limit for instances. However, it will try to reuse
	 * instances as much as possible, but if there is a need for an instance and
	 * the pool is empty, it will not hesitate a create a new instance.
	 * 
	 * @author Jakub Závěrka
	 */
	private class ActionListenerPool
	{
		Collection<CheckBoxListener> pool = new ArrayList<CheckBoxListener>();
				
		/**
		 * This will return an empty instance of <code>CheckBoxListener</code>. If
		 * pool is not empty, then instances will get recycled, but if it is not,
		 * a new instance is created using constructor.
		 * 
		 * @return An instance of <code>CheckBoxListener</code>.
		 */
		CheckBoxListener getInstance(){
			for(CheckBoxListener listener : pool){
				return listener;
			}
			return new CheckBoxListener();
		}
		
		/**
		 * Sets provided instance as suitable for recycling - "returns" an
		 * instance to the pool. The listener will get reset (by calling
		 * <code>setCheckBox(null)</code>) and added to the pool.
		 * 
		 * @param listener
		 *           Listener to return.
		 */
		void returnInstance(CheckBoxListener listener){
			listener.setCheckBox(null);
			pool.add(listener);
		}
	}
	
	/**
	 * Listener of slave checkbox state. Each listener has its
	 * <code>JCheckBox</code> and handles event from its checkbox. This class
	 * will register itself as either <code>ActionListener</code> for ordinary
	 * <code>JCheckBox</code>es, or as <code>ButtonStateChangedListener</code>
	 * for <code>ThreestateCheckBox</code>es.<br>
	 * This listener cas be also deactivated. Deactivated listener will receive
	 * changes from the associated checkbox, but will not respond to them. This
	 * is used when a "master" checkbox needs set state of all of its slaves - we
	 * don't want to respond to actions we ourselves have caused.<br>
	 * This class has default constructor.
	 * 
	 * @author Jakub Závěrka
	 */
	private class CheckBoxListener implements ActionListener, 
		ButtonStateChangedListener
	{
		JCheckBox checkBox;
		boolean activated = true;
				
		/**
		 * Returns checkbox associated to this listener.
		 * @return
		 */
		JCheckBox getCheckBox(){
			return checkBox;
		}

		/**
		 * Sets the associated checkbox to the one provided. This listener removes
		 * itself from the old checkbox and adds to the new one. Providing
		 * <code>null</code> will re-set this listener. Re-setting the listener is
		 * used by the <code>ActionListenerPool</code>
		 * 
		 * @param checkBox
		 *           New checkbox to be set, or <code>null</code> to reset the
		 *           listener.
		 */
		void setCheckBox(JCheckBox checkBox){
			if(this.checkBox != null){
				if(checkBox instanceof ThreestateCheckBox){
					((ThreestateCheckBox)checkBox).
					removeButtonStateChangedListener(this);
				}
				else{
					checkBox.removeActionListener(this);
				}
			}
			
			this.checkBox = checkBox;
			
			if(this.checkBox != null){
				if(checkBox instanceof ThreestateCheckBox){
					((ThreestateCheckBox)checkBox).
					addButtonStateChangedListener(this);
				}
				else{
					checkBox.addActionListener(this);
				}
			}
		}

		/**
		 * Returns current state of the associated checkbox. If the associated
		 * checkbox is an instance of <code>ThreestateCheckBox</code>, it will
		 * return the <code>State</code> of the checkbox. Otherwise, it will
		 * return the corresponding state of the checkbox selection (
		 * <code>State.SELECTED</code> for <code>true</code>,
		 * <code>State.NOT_SELECTED</code> for <code>false</code>).
		 * 
		 * @return State of the associated checkbox.
		 */
		State getCurrentState(){
			State state = null;
			if(checkBox instanceof ThreestateCheckBox){
				state = ((ThreestateCheckBox)checkBox).getState();
			}
			else{
				state = checkBox.isSelected() ? State.SELECTED : State.NOT_SELECTED; 
			}
			return state;
		}

		/**
		 * Returns whether this listener is activated.
		 */
		boolean isActivated(){
			return activated;
		}

		/**
		 * Sets this listener as either activated (<code>true</code>) or
		 * deactivated (</code>false</code>).
		 */
		void setActivated(boolean activated){
			this.activated = activated;
		}

		/**
		 * This gets called when the listener receives a fired event. If the
		 * listener is activated, it notifies the master checkbox that the
		 * state of one of its slaves got changed and needs to be repainted.
		 */
		void eventFired(){
			if(activated){
				stateChanged();
			}
		}
		
		/**
		 * Fired by non-<code>ThreestateCheckBox</code> checkboxes.
		 */
		@Override public void actionPerformed(ActionEvent e){
			eventFired();
		}

		/**
		 * Fired by <code>ThreestateCheckBox</code>es.
		 */
		@Override public void buttonStateChanged(ActionEvent e, State oldState, State newState){
			eventFired();
		}
	}
	
	/**
	 * Test "main" method.
	 * @param args
	 */
	public static void main(String[] args){
		JFrame f = new JFrame();
		f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		Box content = new Box(BoxLayout.Y_AXIS);
		Collection<JCheckBox> slaves = new HashSet<JCheckBox>();
			ThreestateCheckBox slave1 = new ThreestateCheckBox("Slave1");
			slave1.setMargin(new Insets(0, 10, 0, 0));
			slaves.add(slave1);
			ThreestateCheckBox slave2 = new ThreestateCheckBox("Slave2");
			slave2.setMargin(new Insets(0, 10, 0, 0));
			slaves.add(slave2);
			ThreestateCheckBox slave3 = new ThreestateCheckBox("Slave3");
			slave3.setMargin(new Insets(0, 10, 0, 0));
			slaves.add(slave3);
			Collection<JCheckBox> slaves2 = new HashSet<JCheckBox>();
			ThreestateCheckBox slave14 = new ThreestateCheckBox("Slave14");
			slave14.setMargin(new Insets(0, 20, 0, 0));
			slaves2.add(slave14);
			ThreestateCheckBox slave15 = new ThreestateCheckBox("Slave15");
			slave15.setMargin(new Insets(0, 20, 0, 0));
			slaves2.add(slave15);
			ThreestateCheckBox slave16 = new ThreestateCheckBox("Slave16");
			slave16.setMargin(new Insets(0, 20, 0, 0));
			slaves2.add(slave16);
			ObserverThreestateCheckBox slave4 = new ObserverThreestateCheckBox("Slave4", slaves2);
			slave4.setMargin(new Insets(0, 10, 0, 0));
			slaves.add(slave4);
		ObserverThreestateCheckBox master = new ObserverThreestateCheckBox("Master", slaves);
		content.add(master);
		content.add(slave1);
		content.add(slave2);
		content.add(slave3);
		content.add(slave4);
		content.add(slave14);
		content.add(slave15);
		content.add(slave16);
		f.setContentPane(content);
		f.pack();
		f.setVisible(true);
	}
}
